require 'object_proxy'
require 'active_support'
require 'xml/libxml'


# KindDom provides fail-safe access to the the DOM of an XML document using
# three methods: 
# - #collection_of to select a collection of nodes
# - #first_of to select one node
# - #content_for to get node content.
#
# The original libxml behavior of the parsed document is preserved.
#
# As a contrived example, in the controller:
#    @results = KindDom.new(xml_data)
# 
# In the view:
#    <% @results.first_of('//item') do |item| -%>
#     <!-- this block is only executed if `item` is found -->
#   
#      <% item.content_for('title') do |title| -%>
#        <!-- this block is only executed if `title` has content -->
#        <p><%=h title %></p>
#      <% end -%>
#    
#      <% item.content_for('description') do |description| -%>
#        <!-- this block is only executed if `description` has content -->
#        <p><%=h description %></p>
#      <% end -%>
#    <% end -%>
#
class KindDom < ObjectProxy
  
  attr_accessor :dom
  
  def initialize(raw_xml=nil)
    unless raw_xml.nil?
      parser = XML::Parser.new
      parser.string = raw_xml
      @dom = parser.parse
    end
  rescue XML::Parser::ParseError
  ensure
    @dom.extend Kindness
    super(@dom) # Proxy all method calls to @dom. 
  end
  
  module Kindness
    # Retrieve the contents of the first node or attribute selected by the XPath.
    #
    # Optional second argument is the default value to return if the DOM find fails.
    #
    # When a block is provided, it will be used to transform the found content 
    # (or default) before returning.
    #
    # The block should except one argument: the content to tranform.
    #
    def content_for(xpath, default=nil)
      node = case self.class.to_s
      when 'XML::Document' then
        root.find_first(xpath)
      else # 'XML::Node'
        find_first(xpath)
      end
      content = case node.class.to_s
      when 'XML::Attr' then
        node.value
      else
        node.content
      end
    rescue NoMethodError
    ensure
      content = content.blank? ? default : content
      if block_given? and !content.blank?
        return yield(content)
      else
        return content
      end
    end
    
    # Retrieve a collection (Array) of DOM nodes selected by the XPath.
    #
    # Each node is extended with Kindness to support #content_for, #collection_of & #first_of.
    #
    # When a block is provided, it will be used to transform the found collection 
    # (or default) before returning.
    #
    def collection_of(xpath, default=nil)
      c = find(xpath).collect {|n| n.extend Kindness }
    rescue NoMethodError
    ensure
      c = c.blank?||c.size<1 ? default : c
      if block_given? and !c.nil?
        return yield(c)
      else
        return c
      end
    end
    
    # Retrieve the first DOM node selected by the XPath.
    #
    # The node is extended with Kindness to support #content_for, #collection_of & #first_of.
    #
    # When a block is provided, it will be used to transform the found node 
    # (or default) before returning.
    #
    def first_of(xpath, default=nil)
      n = case self.class.to_s
      when 'XML::Document' then
        root.find_first(xpath)
      else # 'XML::Node'
        find_first(xpath)
      end
      n.extend Kindness
    rescue NoMethodError
    ensure
      n = n.blank? ? default : n
      if block_given? and !n.blank?
        return yield(n)
      else
        return n
      end
    end
  end
  
end
